package org.melanesia.beans;

import java.util.Map;
import java.util.logging.Logger;

import org.melanesia.conf.Config;

/**
 * PropertyAccessor class works as a helper class for reflection-like setting /
 * and getting property values of java beans. Note that this class can support
 * composite properties, not only simple properties.
 *
 * @author marcin.kielar
 */
public class PropertyAccessor {

    /** Logger. */
    private static Logger logger = Logger.getLogger(PropertyAccessor.class.getName());

    /**
     * Is property path optional? Property path is optional, when it ends with a
     * '?' character. For such properties, calling
     * {@link #set(Object, Object, Map)} with <code>null</code> value has no
     * effect and is ignored.
     *
     * This allows to handle situations when a mandatory property of an optional
     * parent property would be set to null. For example, we have a following
     * model:
     *
     * <pre>
     * {@code
     *
     * public class Address {
     *   public String streetName;
     *   public int houseNo;
     * }
     *
     * public class Person {
     * 	 public Address address;
     * }
     * </pre>
     *
     * and and we are setting this property as follows:
     *
     * <pre>
     * {@code
     *
     * PropertyAccessor propertyAccessor = AccessorFactory.createAccessor(Person.class, "address.houseNo")
     * propertyAccessor.set(person, mHouseNo, null);
     *
     * }
     * </pre>
     *
     * In such situation, propertyAccessor will contain {@link CompositeSetter}
     * and {@link CompositeGetter}, which will create an instance of the
     * {@code Address} class in order to set the value of the {@code houseNo}
     * field, according to given property path {@code address.houseNo}.
     *
     * Now, if {@code mHouseNo} is null, the setter will fail with an exception,
     * because you cannot set null to a primitive type property (here: int).
     *
     * However, one can specify, that the property is on some level optional by
     * using '?' character inside the property path definition. This way, one
     * can write:
     *
     * <pre>
     * {@code
     *
     * PropertyAccessor propertyAccessor = AccessorFactory.createAccessor(Person.class, "address.houseNo?")
     * propertyAccessor.set(person, mHouseNo, null);
     *
     * }
     * </pre>
     *
     * In the above situation, Melanesia will check whether {@code mHousNo} is
     * null, and if it is, the property path will not be resolved, thus no
     * instance of the {@code Address} class will be created and no attempt will
     * be made to set the {@code houseNo} property of {@code Address} class to a
     * null value.
     *
     */
    private final boolean propertyPathOptional;

    /** Property name that this accessor is reading / writing. */
    private final String propertyName;

    /** Property setter. */
    private final Setter setter;

    /** Property getter. */
    private final Getter getter;

    /**
     * Constructs new {@link PropertyAccessor} with given property name, setter
     * and getter.
     *
     * @param propertyName
     *            property name
     * @param setter
     *            property setter
     * @param getter
     *            property getter
     */
    PropertyAccessor(final String propertyName, final Setter setter, final Getter getter) {

        this.propertyPathOptional = propertyName.endsWith("?");
        this.propertyName = propertyName;
        this.setter = setter;
        this.getter = getter;
    }

    /**
     * Returns property type. This is a "shortcut" function for
     * {@link Getter#getReturnType()}.
     *
     * @return property type
     */
    public final Class<?> getPropertyClass() {
        return getter.getReturnType();
    }

    /**
     * Sets the property value of a given bean. If this is a composite property
     * accessor, it might be useful to pass it not null
     * <code>intermediateBeanClasses</code> in a situation when some of the
     * properties on the composite property path are declared using interfaces,
     * or should be instantiated using some subclass of the declared type.
     *
     * @param bean
     *            bean to access property
     * @param value
     *            value to set
     * @param intermediateBeanClasses
     *            collection of classes (propertyName => class) for bean
     *            instantiation
     */
    public final void set(final Object bean, final Object value, final Map<String, Class<?>> intermediateBeanClasses) {
        if (value == null && propertyPathOptional) {
            if (Config.isDebugMode()) {
                logger.warning("Optional property \"" + propertyName
                        + "\" is being set with a null value. The \"set\" operation will be ignored.");
            }
            return;
        }
        setter.set(bean, value, intermediateBeanClasses);
    }

    /**
     * Returns property value of given bean.
     *
     * @param bean
     *            bean to access property
     * @return property value
     */
    public final Object get(final Object bean) {
        return getter.get(bean);
    }

    /**
     * Returns property name of this accessor.
     *
     * @return property name
     */
    public final String getPropertyName() {
        return propertyName;
    }
}
